
使用光照贴图的好处是我们可以不局限于阴影的最大距离,烘焙的阴影在最大距离之外也不会剔除。通常情况下,我们可以设置在阴影最大距离之内使用实时阴影,超出范围则使用烘焙阴影。
6.1.1 Shadowmask照明模式
阴影蒙版(Shadowmask)是一种纹理,它和与之搭配使用的光照贴图纹理使用相同的UV采样坐标和纹理分辨率。阴影蒙版的每一个纹素,存储着它对应场景位置点上面最多4个光源(因为目前的GPU架构,一个纹素最多只支持RGBA 4个颜色通道)在该位置的遮挡信息,即记录这一点中有多少个光源能照到。

在Lighting Settings的Mixed Lighting中我们把LightingMode改为Shadowmask照明模式。此模式下Unity会先计算从静止的物体投射到其它静止物体上的阴影,即间接照明提供的阴影。把它们存储到一个单独Shadowmask纹理中,如果某处有超过4个光源产生的阴影,则多出来的混合模式光源会转用烘焙式光照计算,具体哪个光源由烘焙式光照计算阴影由引擎决定,每个光照探针最多可以存储4个光源的遮挡信息,如果4个以上的光源发出的光线相交,其余混合模式光源则会自动改为使用烘焙模式,并且这些光源信息是预先算好的。因为混合模式光源的阴影蒙版在运行时有所保留,所以运动的游戏对象所投射的阴影可以与预先计算并存储在阴影蒙版中的阴影正确合成,不会导致重复投影问题。
这个模式中,间接照明效果和阴影衰减都存储在光照贴图中。阴影被存储在额外的Shadowmask纹理中,当只有主光源时,所有被照亮的物体都会作为红色出现在Shadowmask中,红色是因为阴影信息存储在纹理的Red通道中,贴图可以存储4个光照的阴影,因为它只有4个通道。
在Shadowmask模式下,静止的物体向静止的物体投射阴影时是不受阴影的Shadow Distance限制的,只有运动的物体向静止的物体投射阴影才受限制,且要在阴影最大距离内才生效,此部分阴影通过ShadowMap实现。同时运动的游戏对象也可以从静止的游戏对象处接受阴影投射,而这部分阴影是通过光照探针实现的。一般Shadowmask纹理比实时阴影的ShadowMap纹理分辨率低。Unity会自动对静态和动态游戏对象生成的重叠阴影进行组合,因为控制静态物体的光照与阴影蒙版和控制动态物体的光照与阴影贴图将会被编码为遮蔽信息(Occlusion Information)。
我们使用和上节一样的场景,然后调整阴影最大距离,使得一部分物体不产生实时阴影。

有两种方法可以使用阴影蒙版混合照明,我们将Quality->Shadows->Shadowmask Mode设置为Distance Shadowmask模式,我们后续详细介绍。

现在重新烘焙场景,光照贴图的间接光照和之前无异,但是对应的会多出来一张Shadowmask纹理。

6.1.2 使用ShadowMask
1. 要使用阴影蒙版,渲染管线必须先知道它的存在。在Shadows脚本中我们定义一个阴影遮罩关键字数组(因为有两种混合模式,所以使用数组)来控制是否使用阴影蒙版。
static string[] shadowMaskKeywords =
{
"_SHADOW_MASK_DISTANCE"
};
2. 定义一个布尔变量追踪是否使用ShadowMask,并在Setup方法中初始化为false。在Render方法的末尾每帧调用时根据该bool值来启用或禁用该关键字 。
bool useShadowMask;
public void Setup(ScriptableRenderContext context, CullingResults cullingResults,ShadowSettings settings)
{
...
useShadowMask = false;
}
public void Render()
{
if (ShadowedDirectionalLightCount > 0)
{
RenderDirectionalShadows();
}
//是否使用阴影蒙版
buffer.BeginSample(bufferName);
SetKeywords(shadowMaskKeywords, useShadowMask ? 0 : -1);
buffer.EndSample(bufferName);
ExecuteBuffer();
}
3. 要知道是否使用ShadowMask,需要检查是否有使用它的光源,在ReserveDirectionalShadows方法中进行判断。每个光源都包含其烘焙数据信息,可以通过Light.bakingOutput属性获取它,如果光源的光照贴图的烘焙类型为Mixed模式且混合照明模式为Shadowmask,则说明我们在使用ShadowMask。
public Vector3 ReserveDirectionalShadows (Light light, int visibleLightIndex)
{
if (…)
{
//如果使用了ShadowMask
LightBakingOutput lightBaking = light.bakingOutput;
if (lightBaking.lightmapBakeType == LightmapBakeType.Mixed && lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask)
{
useShadowMask = true;
}
...
}
return Vector3.zero;
}
4. 这时需要启用相关关键字,我们先在Lit.shader的CustomLit Pass中声明这个关键字。
#pragma multi_compile _ _SHADOW_MASK_DISTANCE
#pragma multi_compile _ LIGHTMAP_ON
5. 在着色器中我们需要知道是否在使用ShadowMask,如果使用了,要得到烘焙的阴影数据。我们在Shadows.hlsl文件中定义一个ShadowMask结构体存储烘焙阴影数据,bool类型的distance用于标记了阴影蒙版是否使用了Distance Shadow Mask模式,shadows用于存储烘焙的阴影数据。在ShadowData结构体中定义该ShadowMask对象,并在GetShadowData方法中初始化这两个字段。
//烘焙阴影数据
struct ShadowMask
{
bool distance;
float4 shadows;
};
//表面的阴影数据
struct ShadowData
{
...
ShadowMask shadowMask;
};
ShadowData GetShadowData (Surface surfaceWS)
{
ShadowData data;
data.shadowMask.distance = false;
data.shadowMask.shadows = 1.0;
...
}
6. Shadowmask也是场景烘焙照明数据的一部分,因此在GI结构体中也定义一个Shadowmask对象,并在GetGI方法中初始化数据。
struct GI
{
float3 diffuse;
ShadowMask shadowMask;
};
GI GetGI (float2 lightMapUV, Surface surfaceWS)
{
...
gi.shadowMask.distance = false;
gi.shadowMask.shadows = 1.0;
return gi;
}
7. 在GI.hlsl中定义阴影蒙版纹理和相关采样器。
TEXTURE2D(unity_ShadowMask);
SAMPLER(samplerunity_ShadowMask);
8. 然后定义一个SampleBakedShadows方法使用光照贴图的UV坐标对阴影蒙版纹理进行采样,仅在使用了光照贴图的时候执行此操作,否则烘焙阴影衰减为1。
//采样shadowMask得到烘焙阴影数据
float4 SampleBakedShadows (float2 lightMapUV)
{
#if defined(LIGHTMAP_ON)
return SAMPLE_TEXTURE2D(unity_ShadowMask, samplerunity_ShadowMask, lightMapUV);
#else
return 1.0;
#endif
}
9. 然后在GetGI方法中进行判断,如果启用了_SHADOW_MASK_DISTANCE关键字,则采样纹理并获得烘焙阴影数据。
GI GetGI(float2 lightMapUV, Surface surfaceWS)
{
...
#if defined(_SHADOW_MASK_DISTANCE)
gi.shadowMask.distance = true;
gi.shadowMask.shadows = SampleBakedShadows(lightMapUV);
#endif
return gi;
}
10. 在Lighting文件的GetLighting方法中将GI的Shadowmask数据复制到ShadowData中。我们进行测试,直接返回采样阴影蒙版纹理获取的烘焙阴影数据作为最终照明结果来查看效果。
float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi)
{
//得到表面阴影数据
ShadowData shadowData = GetShadowData(surfaceWS);
shadowData.shadowMask = gi.shadowMask;
return gi.shadowMask.shadows.rgb;
...
}
11. 现在会发现场景中所有对象都变成了白色,因为我们还没有把Shadowmask数据发送到GPU,在CameraRenderer脚本的DrawVisibleGeometry方法中创建drawingSettings实例的时候进行设置。
perObjectData = PerObjectData.Lightmaps | PerObjectData.ShadowMask | PerObjectData.LightProbe | PerObjectData.LightProbeProxyVolume

6.1.3 遮挡探针(Occlusion Probe)
1. 如上图所示,阴影蒙版已经被正确地应用到了静态的光照贴图对象,但是动态对象(球体)还没有阴影蒙版数据,因为它们使用的是光照探针而不是光照贴图,然而Unity也会将阴影蒙版数据烘焙到光照探针中,称之为遮挡探针(Occlusion Probes)。我们可以通过在UnityInput文件的UnityPerdraw缓冲区中添加一个unity_ProbesOcclusion向量来访问这些数据。
real4 unity_WorldTransformParams;
float4 unity_ProbesOcclusion;
float4 unity_LightmapST;
float4 unity_DynamicLightmapST;
2. 在GI.hlsl的SampleBakedShadows方法中,在没有使用光照贴图的情况下返回遮挡探针数据。
//采样shadowMask
float4 SampleBakedShadows (float2 lightMapUV)
{
#if defined(LIGHTMAP_ON)
return SAMPLE_TEXTURE2D(unity_ShadowMask, samplerunity_ShadowMask, lightMapUV);
#else
return unity_ProbesOcclusion;
#endif
}
3. 在CameraRenderer脚本的DrawVisibleGeometry方法中添加PerObjectData.OcclusionProbe的标志把遮挡探针数据发送到GPU。
perObjectData = PerObjectData.Lightmaps | PerObjectData.ShadowMask | PerObjectData.LightProbe | PerObjectData.OcclusionProbe | PerObjectData.LightProbeProxyVolume

上图是采样遮挡探针的效果。对探针来说,阴影蒙版没有使用的通道被设置为白色,所以动态物体处在完全照明时为白色,处在完全阴影中显示为青色,而不是红色和黑色。
4. 虽然这足以让阴影蒙版通过探针的方式工作,但它会打断GPU Instancing的合批。UnityInstancing只有在定义SHADOWS_SHADOWMASK宏时,遮挡数据才可以自动得到实例。因此在Common文件中include UnityInstancing.hlsl文件之前定义该宏。
#if defined(_SHADOW_MASK_DISTANCE)
#define SHADOWS_SHADOWMASK
#endif
#include
"Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
5. LPPV也可以和阴影遮罩配合使用,同样在CameraRenderer.DrawVisibleGeometry方法中添加一个PerObjectData.OcclusionProbeProxyVolume标志。
perObjectData = PerObjectData.Lightmaps | PerObjectData.ShadowMask | PerObjectData.LightProbe | PerObjectData.OcclusionProbe | PerObjectData.LightProbeProxyVolume | PerObjectData.OcclusionProbeProxyVolume
6. 采样LPPV的遮挡数据方法和采样LPPV的光照数据基本一样,我们在GI文件的SampleBakedShadows方法中使用SampleProbeOcclusion方法进行采样,它跟SampleProbeVolumeSH4方法使用的参数基本一样,除非它不再需要法线向量。
//采样shadowMask
float4 SampleBakedShadows (float2 lightMapUV, Surface surfaceWS)
{
#if defined(LIGHTMAP_ON)
return SAMPLE_TEXTURE2D(unity_ShadowMask, samplerunity_ShadowMask, lightMapUV);
#else
if (unity_ProbeVolumeParams.x)
{
//采样LPPV遮挡数据
return SampleProbeOcclusion(TEXTURE3D_ARGS(unity_ProbeVolumeSH, samplerunity_ProbeVolumeSH),surfaceWS.position,
unity_ProbeVolumeWorldToObject,unity_ProbeVolumeParams.y, unity_ProbeVolumeParams.z,unity_ProbeVolumeMin.xyz, unity_ProbeVolumeSizeInv.xyz);
}
else
{
return unity_ProbesOcclusion;
}
#endif
}
7. 最后在GetGI方法中调用SampleBakedShadows方法的时候传递表面数据。
gi.shadowMask.shadows = SampleBakedShadows(lightMapUV, surfaceWS);

8. 如果我们的Mesh Ball脚本中使用了LPPV,它也就支持阴影蒙版了,但是当它对光照探针进行插值时,我们需要添加遮挡探针数据。首先声明一个存储遮挡探针数据的数组,然后调用CalculateInterpolatedLightAndOcclusionProbes方法时添加该数组作为第三个传参,用于填充遮挡探针数据。最后调用CopyProbeOcclusionArrayFrom方法将数据传递到材质属性块中。
此时阴影蒙版的数据已经正确地发送到了着色器中,现在可以在Lighting文件的GetLighting方法中把测试代码删掉了。
void Update()
{
if (block == null)
{
...
var lightProbes = new SphericalHarmonicsL2[1023];
var occlusionProbes = new Vector4[1023];
LightProbes.CalculateInterpolatedLightAndOcclusionProbes(positions, lightProbes, occlusionProbes);
block.CopySHCoefficientArraysFrom(lightProbes);
block.CopyProbeOcclusionArrayFrom(occlusionProbes);
}
...
}
//return gi.shadowMask.shadows.rgb;
下一步我们将烘焙阴影和实时阴影进行混合,在超过阴影最大距离时使用烘焙阴影,距离之内使用实时阴影。
6.2.1 实现
1. 混合阴影会让GetDirectionalShadowAttenuation方法更臃肿,我们将其中的实时阴影采样的代码抽离出来,封装到一个新的GetCascadedShadow方法中。
float GetCascadedShadow(DirectionalShadowData directional, ShadowData global, Surface surfaceWS)
{
//计算法线偏差
float3 normalBias = surfaceWS.normal * (directional.normalBias * _CascadeData[global.cascadeIndex].y);
float3 positionSTS = mul(_DirectionalShadowMatrices[directional.tileIndex], float4(surfaceWS.position + normalBias, 1.0)).xyz;
float shadow = FilterDirectionalShadow(positionSTS);
if (global.cascadeBlend < 1.0)
{
normalBias = surfaceWS.normal *(directional.normalBias * _CascadeData[global.cascadeIndex + 1].y);
positionSTS = mul(_DirectionalShadowMatrices[directional.tileIndex + 1], float4(surfaceWS.position + normalBias, 1.0)).xyz;
shadow = lerp(FilterDirectionalShadow(positionSTS), shadow, global.cascadeBlend);
}
return shadow;
}
//计算阴影衰减
float GetDirectionalShadowAttenuation(DirectionalShadowData directional, ShadowData global, Surface surfaceWS)
{
#if !defined(_RECEIVE_SHADOWS)
return 1.0;
#endif
float shadow;
if (directional.strength <= 0.0)
{
shadow = 1.0;
}
else
{
shadow = GetCascadedShadow(directional, global, surfaceWS);
shadow = lerp(1.0, shadow, directional.strength);
}
return shadow;
}
2. 然后定义一个GetBakedShadow方法,通过给定阴影蒙版得到烘焙阴影的衰减值,如果Quality->Shadows->Shadowmask Mode使用的是Distance Shadowmask模式,我们返回烘焙阴影数据的R分量,否则烘焙阴影衰减为1。
//得到烘焙阴影的衰减值
float GetBakedShadow(ShadowMask mask)
{
float shadow = 1.0;
if (mask.distance)
{
shadow = mask.shadows.r;
}
return shadow;
}
3. 创建一个MixBakedAndRealtimeShadows方法用来混合烘焙和实时阴影,传参为阴影数据,实时阴影衰减和灯光阴影强度。如果Shadowmask Mode为Distance Shadowmask,则用烘焙阴影的衰减替换实时阴影衰减。
//混合烘焙和实时阴影
float MixBakedAndRealtimeShadows(ShadowData global, float shadow, float strength)
{
float baked = GetBakedShadow(global.shadowMask);
if (global.shadowMask.distance)
{
shadow = baked;
}
return lerp(1.0, shadow, strength);
}
4. 在GetDirectionalShadowAttenuation方法的最后,调用混合阴影的方法得到最终阴影衰减。
//得到阴影衰减
float GetDirectionalShadowAttenuation(DirectionalShadowData directional, ShadowData global, Surface surfaceWS)
{
...
else
{
shadow = GetCascadedShadow(directional, global, surfaceWS);
//shadow = lerp(1.0, shadow, directional.strength);
//阴影混合
shadow = MixBakedAndRealtimeShadows(global, shadow, directional.strength);
}
return shadow;
}

现在我们场景中始终使用的都是阴影蒙版,但是它会跟实时阴影一样超过最大阴影距离就消失掉了。
6.2.2 阴影过渡
1. 现在我们需要根据深度来从实时阴影过渡到烘焙阴影,且根据全局阴影强度在它们两者之间进行插值。然而我们还需在插值之后应用光源的阴影强度,因此不能在GetDirectionalShadowData方法中直接将光源的阴影强度和方向光阴影数据相乘。
DirectionalShadowData GetDirectionalShadowData(int lightIndex, ShadowData shadowData)
{
DirectionalShadowData data;
//data.strength = _DirectionalLightShadowData[lightIndex].x * shadowData.strength;
data.strength = _DirectionalLightShadowData[lightIndex].x;
...
}
2. 在Shadows.hlsl的MixBakedAndRealtimeShadows方法中使用全局阴影强度在烘焙阴影和实时阴影之间进行插值,插值后的阴影衰减再和灯光的阴影强度进行插值。如果没有使用阴影遮罩,则把灯光阴影强度乘上全局的阴影强度。
//混合烘焙和实时阴影
float MixBakedAndRealtimeShadows(ShadowData global, float shadow, float strength)
{
float baked = GetBakedShadow(global.shadowMask);
if (global.shadowMask.distance)
{
shadow = lerp(baked, shadow, global.strength);
return lerp(1.0, shadow, strength);
}
return lerp(1.0, shadow, strength * global.strength);
}
现在阴影已经可以正常过渡了,但只有在实时阴影被渲染出来时才生效,如果将视野拉远一些,超过了阴影最大距离,不仅实时阴影会消失,烘焙阴影也会消失。我们需要在没有实时阴影的时候让烘焙阴影显示正常。

3. 创建一个重载方法GetBakedShadow(),根据传入的灯光阴影强度对烘焙阴影进行插值得到烘焙阴影的衰减值。
float GetBakedShadow(ShadowMask mask, float strength)
{
if (mask.distance)
{
return lerp(1.0, GetBakedShadow(mask), strength);
}
return 1.0;
}
4. 接下来修改GetDirectionalShadowAttenuation方法的if判断条件,当方向光强度和全局阴影强度相乘小于等于0时,不再返回1,而是调用GetBakedShadow方法返回烘焙阴影的衰减值。
if (directional.strength * global.strength <= 0.0)
{
shadow = GetBakedShadow(global.shadowMask, directional.strength);
}
5. 调整Shadow脚本的ReserveDirectionalShadows方法,删除if语句中最后一项判断条件,改为在if语句内进行判断,主要确定光源是否使用了阴影遮罩,即使没有阴影投射,也返回光源的阴影强度。但当阴影强度大于零时,着色器将会采样阴影贴图,在这里是不对的,这里我们将返回的灯光阴影强度取反来解决这个问题。
public Vector3 ReserveDirectionalShadows(Light light, int visibleLightIndex)
{
if (ShadowedDirectionalLightCount < maxShadowedDirectionalLightCount && light.shadows != LightShadows.None && light.shadowStrength > 0f)
{
//如果使用了ShadowMask
LightBakingOutput lightBaking = light.bakingOutput;
if (lightBaking.lightmapBakeType == LightmapBakeType.Mixed && lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask)
{
useShadowMask = true;
}
if (!cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b ))
{
return new Vector3(-light.shadowStrength, 0f, 0f);
}
...
}
return Vector3.zero;
}
6. 在Shadows.hlsl的GetDirectionalShadowAttenuation方法中获取烘焙阴影衰减时,传递的是灯光阴影强度的绝对值,这样即使在阴影最大距离外或者关闭了实时阴影的投射也可以得到正确的烘焙阴影。
shadow = GetBakedShadow(global.shadowMask, abs(directional.strength));

6.2.3 Shadowmask模式
Quality->Shadows的Shadowmask Mode还有一种Shadowmask模式,它的工作原理和Distance Shadowmask相同,但在这个模式下Unity会把静态物体的实时阴影替换为烘焙阴影。这样意味着需要渲染的实时阴影变少,使得渲染效率变高,代价是距离视野比较近的静态阴影质量会较低,现在我们来让渲染管线支持这个模式。

1. 在Shadows脚本的shadowMaskKeywords数组中添加_SHADOW_MASK_ALWAYS关键字,并作为数组的第一个元素。然后在Render方法中设置关键字时判断使用了哪种shadowmaskMode。
static string[] shadowMaskKeywords =
{
"_SHADOW_MASK_ALWAYS",
"_SHADOW_MASK_DISTANCE"
};
SetKeywords(shadowMaskKeywords, useShadowMask ? QualitySettings.shadowmaskMode == ShadowmaskMode.Shadowmask ? 0 : 1 : -1);
2. 在Lit.shader中添加该关键字。
#pragma multi_compile _ _SHADOW_MASK_ALWAYS _SHADOW_MASK_DISTANCE
3. 在Common.hlsl中也添加该关键字的定义判断。
#if defined(_SHADOW_MASK_ALWAYS) || defined(_SHADOW_MASK_DISTANCE)
#define SHADOWS_SHADOWMASK
#endif
4. 在Shadows文件的ShadowMask结构体中添加一个布尔值always作为使用Shadowmask模式的标记,并在GetShadowData方法中给该字段默认设为false。
struct ShadowMask
{
bool always;
bool distance;
float4 shadows;
};
ShadowData GetShadowData (Surface surfaceWS)
{
ShadowData data;
data.shadowMask.always = false;
...
}
5. 在GI.hlsl的GetGI方法中当该关键字被定义时,设置该bool字段的标识和获得烘焙阴影数据。
GI GetGI(float2 lightMapUV, Surface surfaceWS)
{
...
gi.shadowMask.always = false;
gi.shadowMask.distance = false;
gi.shadowMask.shadows = 1.0;
#if defined(_SHADOW_MASK_ALWAYS)
gi.shadowMask.always = true;
gi.shadowMask.shadows = SampleBakedShadows(lightMapUV, surfaceWS);
#elif defined(_SHADOW_MASK_DISTANCE)
gi.shadowMask.distance = true;
gi.shadowMask.shadows = SampleBakedShadows(lightMapUV, surfaceWS);
#endif
return gi;
}
6. 当有任何一种Shadowmask Mode时,两个GetBakedShadow方法都应使用阴影蒙版的数据。
float GetBakedShadow(ShadowMask mask)
{
float shadow = 1.0;
if (mask.always || mask.distance)
{
shadow = mask.shadows.r;
}
return shadow;
}
float GetBakedShadow(ShadowMask mask, float strength)
{
if (mask.always || mask.distance)
{
return lerp(1.0, GetBakedShadow(mask), strength);
}
return 1.0;
}
7. 最后在MixBakedAndRealtimeShadows方法中判断Shadowmask模式启用时,首先通过全局强度对实时阴影进行插值,然后将烘焙阴影和实时阴影进行混合,取两种的最小值,最后通过光源的阴影强度对混合阴影进行插值。
float MixBakedAndRealtimeShadows(ShadowData global, float shadow, float strength)
{
float baked = GetBakedShadow(global.shadowMask);
if (global.shadowMask.always)
{
shadow = lerp(1.0, shadow, global.strength);
shadow = min(baked, shadow);
return lerp(1.0, shadow, strength);
}
...
}

前面提到了Shadowmask纹理有4个通道,所以最多可支持4个Mixed光源,烘焙时,最重要的方向光的阴影信息存储在R通道中,第二个光源的阴影信息存储在B通道中,以此类推。我们接下来进行测试,创建一个新的方向光,保持跟主光源相同的位置并沿Y轴旋转90度,光照强度比主光源调的弱一点。
然后我们进行烘焙,发现阴影蒙版纹理中的光源阴影信息已被正确存储,仅主光源照亮的区域为红色,第二盏灯照亮的区域为绿色,两者都照亮的区域则为黄色。我们发现这2个光源都使用了相同的烘焙阴影,因为目前我们的着色器只使用了R通道的阴影信息,需要将光源的通道索引发送到GPU来进行调整。但我们不能依赖灯光的顺序,因为灯光可以在运行时发生变化,甚至禁用掉。

6.3.1 实现
1. 在Shadows脚本的ReserveDirectionalShadows方法中,可以通过LightBakingOutput的occlusionMaskChannel字段得到光源的阴影蒙版通道索引,将它作为返回值的W分量,返回类型由Vector3改为Vector4,当然如果灯光没有使用ShadowMask时,则设置通道索引为-1。
public Vector4 ReserveDirectionalShadows(Light light, int visibleLightIndex)
{
if (...)
{
float maskChannel = -1;
//如果使用了ShadowMask
LightBakingOutput lightBaking = light.bakingOutput;
if (lightBaking.lightmapBakeType == LightmapBakeType.Mixed && lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask)
{
useShadowMask = true;
maskChannel = lightBaking.occlusionMaskChannel;
}
if (!cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b ))
{
return new Vector4(-light.shadowStrength, 0f, 0f, maskChannel);
}
ShadowedDirectionalLights[ShadowedDirectionalLightCount] = new ShadowedDirectionalLight
{
visibleLightIndex = visibleLightIndex,slopeScaleBias = light.shadowBias,
nearPlaneOffset = light.shadowNearPlane
};
return new Vector4(light.shadowStrength, settings.directional.cascadeCount * ShadowedDirectionalLightCount++, light.shadowNormalBias, maskChannel);
}
return new Vector4(0f, 0f, 0f, -1f);
}
2. 在Shadows.hlsl的DirectionalShadowData结构体中添加一个int类型的通道索引属性。
struct DirectionalShadowData
{
float strength;
int tileIndex;
float normalBias;
int shadowMaskChannel;
};
3. 在Light.hlsl的GetDirectionalShadowData方法中获取从CPU发来的通道索引。
data.shadowMaskChannel = _DirectionalLightShadowData[lightIndex].w;
return data;
4. 将通道索引作为参数传到Shadows.hlsl的两个GetBakedShadow方法中,当通道索引大于0时(光源使用了Shadowmask时),返回对应索引的阴影蒙版中的阴影衰减值。
float GetBakedShadow(ShadowMask mask, int channel)
{
float shadow = 1.0;
if (mask.always || mask.distance)
{
if (channel >= 0)
{
shadow = mask.shadows[channel];
}
}
return shadow;
}
float GetBakedShadow(ShadowMask mask, int channel, float strength)
{
if (mask.always || mask.distance)
{
return lerp(1.0, GetBakedShadow(mask,channel), strength);
}
return 1.0;
}
5. 在MixBakedAndRealtimeShadows方法中调用GetBakedShadow方法时传递通道索引。
float MixBakedAndRealtimeShadows(ShadowData global, float shadow, int shadowMaskChannel, float strength)
{
float baked = GetBakedShadow(global.shadowMask, shadowMaskChannel);
...
}
6. 最后在GetDirectionalShadowAttenuation方法中调用相关函数时传递Shadowmask的通道索引。
float GetDirectionalShadowAttenuation(DirectionalShadowData directional, ShadowData global, Surface surfaceWS)
{
...
if (directional.strength * global.strength <= 0.0)
{
shadow = GetBakedShadow(global.shadowMask, directional.shadowMaskChannel, abs(directional.strength));
}
else
{
shadow = GetCascadedShadow(directional, global, surfaceWS);
//阴影混合
shadow = MixBakedAndRealtimeShadows(global, shadow, directional.shadowMaskChannel, directional.strength);
}
return shadow;
}

6|阴影蒙版
提交
暂无评论